13  Pandas groupby用法

13.1 引言分组聚合的分析价值

Split-Apply-Combine(分组-应用-合并)是数据分析的核心范式:

  1. Split: 将数据分为多个组
  2. Apply: 对每组应用函数
  3. Combine: 合并结果

金融应用场景:

  • 按行业统计股票表现
  • 按时间段计算收益率
  • 按市场分组分析风险

13.2 本章学习目标

通过本章学习,你将能够:

  1. 说出 Split–Apply–Combine 范式三步各自的含义,并解释 groupby 对象”只分组、不计算”的特点
  2. list(grouped) 的输出观察拆分结果,指出每个组对应原表的哪些行
  3. mean/agg 得到”一组一行”的汇总,用 transform 得到与原表同形状的结果,用 filter 做整组去留,用 apply 承接自定义逻辑
  4. 根据”要什么形状的输出”在 agg、transform、filter、apply 四种动作中做出选择
  5. 读懂并改写平台任务中的 groupby(...).transform(lambda ...) 组内标准化代码

先修内容:第 章节 10 章至第 章节 12 章的数据框创建、内部操作与拼接。

13.3 从一张销售表理解 groupby(split–apply–combine)

在进入平台任务之前,我们用一张 8 行的营业部销售表把引言中的三步范式”看”清楚:所谓分组,就是把一张长表按某个键切成若干张小表(split),对每张小表做同样的计算(apply),再把结果拼回一张表(combine)。

列表 13.1: 用一张8行销售表演示groupby的拆分效果
import pandas as pd  # 导入Pandas数据分析库

# ==================== 构造营业部销售明细表 ====================
sales = pd.DataFrame({  # 创建销售明细数据框sales
    '营业部': ['华东一部', '华东一部', '华东二部', '华东二部',  # 分组键:每笔销售所属营业部
              '华北一部', '华北一部', '华北一部', '华南一部'],  # 营业部标签的续行
    '商品': ['笔记本', '平板', '笔记本', '手机',  # 商品名称
            '平板', '耳机', '笔记本', '手机'],  # 商品名称的续行
    '销售额': [12000, 8000, 9000, 15000, 6000, 2000, 11000, 13500],  # 销售额(元)
    '数量': [24, 40, 18, 30, 30, 50, 22, 27]  # 销售数量(件)
})
print('原始销售表(8行明细):')  # 打印标题
print(sales)  # 输出完整的销售明细表

# ==================== split:按营业部把长表切成若干小表 ====================
grouped = sales.groupby('营业部')  # 按营业部分组,得到GroupBy分组对象
print('\nlist(grouped)看到的分组效果——(组名, 子数据框)对:')  # 打印标题
print(list(grouped))  # 输出全部(组名,该组子数据框)元组组成的列表

# ==================== 逐组查看切分结果 ====================
for name, group in grouped:  # 遍历分组:name是组名,group是该组的子数据框
    print(f'\n组名: {name}, 行索引: {list(group.index)}')  # 输出组名与该组包含的行号
    print(group)  # 输出该组的子数据框

list(grouped) 的输出可以看到,grouped 对象里装的是四张”小表”:每个元素是一个 (组名, 子数据框) 元组——华东一部拿到第 0、1 行,华北一部拿到第 4、5、6 行。这就是 split。groupby 本身不做任何计算,它只记住了”谁和谁是一组”;计算发生在后面的 meanaggtransform 等动作里。

13.3.1 第一步:用 mean 得到”一组一行”的汇总

列表 13.2: 第一步:分组均值把8行明细压缩为4行汇总
import pandas as pd  # 导入Pandas数据分析库

# ==================== 重建销售表,使本代码块可独立运行 ====================
sales = pd.DataFrame({  # 重建本章使用的销售明细表
    '营业部': ['华东一部', '华东一部', '华东二部', '华东二部',  # 分组键:营业部
              '华北一部', '华北一部', '华北一部', '华南一部'],  # 营业部标签的续行
    '商品': ['笔记本', '平板', '笔记本', '手机', '平板', '耳机', '笔记本', '手机'],  # 商品名称
    '销售额': [12000, 8000, 9000, 15000, 6000, 2000, 11000, 13500],  # 销售额(元)
    '数量': [24, 40, 18, 30, 30, 50, 22, 27]  # 销售数量(件)
})

# ==================== apply+combine:每组求均值,再拼成以组名为索引的新表 ====================
result = sales.groupby('营业部')[['销售额', '数量']].mean()  # 先选数值列,再分组求均值
print(result)  # 输出:8行明细压缩为4行汇总
print('\n各组包含的明细行数:')  # 打印标题
print(sales.groupby('营业部').size())  # size()统计每组的行数(含缺失值)

combine 阶段把每组的均值拼成一张以组名为索引的新表:8 行变 4 行。注意这里先选了 [['销售额', '数量']] 两列数值列——对含文本列(商品)的整表直接 mean() 会因无法对字符串求均值而报错。

13.3.2 第二步:用 agg 一次计算多个统计量

列表 13.3: 第二步:agg同时计算均值与合计
import pandas as pd  # 导入Pandas数据分析库

# ==================== 重建销售表,使本代码块可独立运行 ====================
sales = pd.DataFrame({  # 重建本章使用的销售明细表
    '营业部': ['华东一部', '华东一部', '华东二部', '华东二部',  # 分组键:营业部
              '华北一部', '华北一部', '华北一部', '华南一部'],  # 营业部标签的续行
    '商品': ['笔记本', '平板', '笔记本', '手机', '平板', '耳机', '笔记本', '手机'],  # 商品名称
    '销售额': [12000, 8000, 9000, 15000, 6000, 2000, 11000, 13500],  # 销售额(元)
    '数量': [24, 40, 18, 30, 30, 50, 22, 27]  # 销售数量(件)
})

# ==================== agg接收函数列表,每列同时算多个统计量 ====================
result = sales.groupby('营业部')[['销售额', '数量']].agg(['mean', 'sum'])  # 每列同时算均值与合计
print(result)  # 输出:列名变为(原列名,统计量)的两层结构

均值与合计经常要一起看:均值回答”平均每一单有多大”,合计回答”这个部整体做了多大”。agg 还可以传入 {'销售额': 'sum', '数量': 'mean'} 这样的字典,让不同列用不同统计量。

13.3.3 第三步:用 transform 得到与原表同形状的结果

前两步都在”压缩”数据。但很多业务问题要回答的是每一行:例如”华东一部的笔记本占本部销售额的百分之几”——分子是本行的销售额,分母是本部的销售额合计。用 agg 得到的是每部一个合计数,长度只有 4,无法直接贴回 8 行明细;而 transform 返回与原列同样长度的结果,索引与原表对齐,可以直接作为新列并回原表。

列表 13.4: 第三步:transform计算每行在本组内的占比
import pandas as pd  # 导入Pandas数据分析库

# ==================== 重建销售表,使本代码块可独立运行 ====================
sales = pd.DataFrame({  # 重建本章使用的销售明细表
    '营业部': ['华东一部', '华东一部', '华东二部', '华东二部',  # 分组键:营业部
              '华北一部', '华北一部', '华北一部', '华南一部'],  # 营业部标签的续行
    '商品': ['笔记本', '平板', '笔记本', '手机', '平板', '耳机', '笔记本', '手机'],  # 商品名称
    '销售额': [12000, 8000, 9000, 15000, 6000, 2000, 11000, 13500],  # 销售额(元)
    '数量': [24, 40, 18, 30, 30, 50, 22, 27]  # 销售数量(件)
})

# ==================== 先定义具名函数,再交给transform ====================
def calc_share(x):  # 定义具名函数:x依次是每个营业部的销售额列
    return x / x.sum()  # 每行销售额除以本组销售额合计,得到组内占比

sales['组内占比'] = sales.groupby('营业部')['销售额'].transform(calc_share)  # 输出与原表同长度
print(sales[['营业部', '商品', '销售额', '组内占比']])  # 查看每行的组内占比
print('\n等价的lambda匿名函数写法:')  # 打印标题
print(sales.groupby('营业部')['销售额'].transform(lambda x: x / x.sum()).round(4))  # 匿名函数版本

calc_sharelambda x: x / x.sum() 完全等价。逻辑简单时用 lambda 更紧凑;逻辑较复杂、或需要在多处复用时,具名函数更清晰可读。这一步也正是平台任务中 transform(lambda x: (x - x.mean()) / x.std()) 组内标准化的原型:两者都是”每行的值,除以/减去本组的统计量”。

13.3.4 第四步:用 filter 按整组去留筛选

第三个常见需求是”整组保留或整组丢弃”:例如公司只分析合计销售额达到一定规模的营业部。filter 对每组调用一个返回 True/False 的函数,返回 True 的组的全部明细行被保留。

列表 13.5: 第四步:filter筛出合计销售额超阈值的组
import pandas as pd  # 导入Pandas数据分析库

# ==================== 重建销售表,使本代码块可独立运行 ====================
sales = pd.DataFrame({  # 重建本章使用的销售明细表
    '营业部': ['华东一部', '华东一部', '华东二部', '华东二部',  # 分组键:营业部
              '华北一部', '华北一部', '华北一部', '华南一部'],  # 营业部标签的续行
    '商品': ['笔记本', '平板', '笔记本', '手机', '平板', '耳机', '笔记本', '手机'],  # 商品名称
    '销售额': [12000, 8000, 9000, 15000, 6000, 2000, 11000, 13500],  # 销售额(元)
    '数量': [24, 40, 18, 30, 30, 50, 22, 27]  # 销售数量(件)
})

# ==================== 先定义具名函数,再交给filter ====================
def sales_over_threshold(group, threshold=15000):  # 定义具名函数:对整组做去留判断
    return group['销售额'].sum() > threshold  # 本组销售额合计超过阈值则返回True

kept = sales.groupby('营业部').filter(sales_over_threshold)  # 只保留返回True的组的全部明细
print(kept[['营业部', '商品', '销售额']])  # 华南一部合计13500元未达标,整组被剔除

注意 filter 与布尔行筛选的区别:sales[sales['销售额'] > 10000] 丢掉的是不满足条件的,而 filter 丢掉的是不满足条件的——华南一部的手机单件销售额 13500 元虽然很高,但整组合计未达阈值,连这一行也一并出局。

13.3.5 第五步:用 apply 承接自定义逻辑

当需求既不是逐组汇总、也不是同形状变换、也不是整组筛选,而是”每组返回一个任意结构的结果”时,用 apply。例如找出每个营业部销售额最高的那件商品:每组返回一行,但这一行怎么挑,由我们自己写的函数决定。

列表 13.6: 第五步:apply找出各营业部的王牌商品
import pandas as pd  # 导入Pandas数据分析库

# ==================== 重建销售表,使本代码块可独立运行 ====================
sales = pd.DataFrame({  # 重建本章使用的销售明细表
    '营业部': ['华东一部', '华东一部', '华东二部', '华东二部',  # 分组键:营业部
              '华北一部', '华北一部', '华北一部', '华南一部'],  # 营业部标签的续行
    '商品': ['笔记本', '平板', '笔记本', '手机', '平板', '耳机', '笔记本', '手机'],  # 商品名称
    '销售额': [12000, 8000, 9000, 15000, 6000, 2000, 11000, 13500],  # 销售额(元)
    '数量': [24, 40, 18, 30, 30, 50, 22, 27]  # 销售数量(件)
})

# ==================== 先定义具名函数,再交给apply ====================
def top_sales_row(group):  # 定义具名函数:返回组内销售额最高的那一行
    return group.loc[group['销售额'].idxmax()]  # idxmax()给出最大值行号,loc取整行

result = sales.groupby('营业部')[['商品', '销售额', '数量']].apply(top_sales_row)  # 每组返回一行
print(result)  # 输出:各营业部的王牌商品,行索引是营业部名

这里先选取 [['商品', '销售额', '数量']]apply,把分组列排除在外,结果更干净;若对含分组列的整表直接 apply,新版 pandas 会提示”对分组列进行操作已弃用”的警告。

13.3.6 四种动作的对照表

方法 输入 输出形状 典型场景
agg(含 mean/sum 等) 每组的一列或多列 每组一行,组名成为索引 各营业部平均销售额、合计销售额
transform 每组的一列 与原列同形状,索引与原表对齐 组内占比、组内标准化、把组统计量贴回明细
filter 每组的完整子数据框 原表的子集,整组保留或整组丢弃 只保留合计销售额超阈值的营业部
apply 每组的完整子数据框 由函数返回值决定(每组一行、一个值或任意结构) 取每组最优商品行等自定义逻辑

记住这张表的”输出形状”一列,平台任务里 grouped.transform(lambda x: ...) 的输出为什么是 6 行而不是 2 行,答案就显而易见了:transform 永远与原表同形状。

13.4 groupby基础

任务要求:给定 6 行、含 A(分组标签)、BCD 四列的数据框,按 A 列分组并选取 CD 两列,用 transform 配合匿名函数 lambda x: (x - x.mean()) / x.std() 做组内标准化,输出与原表同形状的结果。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 13.7: 平台原始代码
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
import pandas as pd  # 导入Pandas数据分析库
df = pd.DataFrame({'A' : ['foo', 'bar', 'foo', 'bar',  # 创建数据框df
             'foo', 'bar'],  # A列分组标签的续行数据
          'B' : ['one', 'one', 'two', 'three',  # "B"的数据序列
             'two', 'two'],  # B列分组标签的续行数据
          'C' : [1, 5, 5, 2, 5, 5],  # "C"的数据序列
          'D' : [2.0, 5., 8., 1., 2., 9.]})  # "D"的数据序列
grouped = df.groupby('A')[['C', 'D']]  # 按指定列分组聚合
result=grouped.transform(lambda x: (x - x.mean()) / x.std())  # 定义匿名函数result
print(result)  # 输出分析结果数据

预期输出(本机 Python 实际运行结果,具体以平台运行结果为准):

          C         D
0 -1.154701 -0.577350
1  0.577350  0.000000
2  0.577350  1.154701
3 -1.154701 -1.000000
4  0.577350 -0.577350
5  0.577350  1.000000

输出为 6 行(与原表同形状)而不是 2 行(组数),这正是 transform 的标志:每行的标准化都以其所在组的均值和标准差为基准,组内均值被消为 0、组内标准差被缩放为 1。

13.5 聚合函数(Aggregation)

列表 13.8: groupby聚合函数的应用
# =============================================================================
# 题目:分组聚合统计
# =============================================================================
# 本示例演示对分组数据应用常用聚合函数
# 金融应用:计算每只股票的平均收益率、每行业的总市值等

# ==================== 选择特定列分组 ====================
# 只对C、D两列进行分组聚合(排除不需要的列)
grouped = df.groupby('A')[['C', 'D']]  # 双重索引选择特定列

# ==================== 应用聚合函数 ====================
# 计算每组的均值
print("均值:")
print(grouped.mean())  # mean()返回每组的平均值,NaN会被自动忽略

# 计算每组的总和
print(f"\n求和:")
print(grouped.sum())  # sum()返回每组的总和

# 计算每组的非缺失值计数
print(f"\n计数:")
print(grouped.count())  # count()统计每组的非空值数量

# 计算每组的标准差
print(f"\n标准差:")
print(grouped.std())  # std()计算标准差,衡量数据波动性

# ==================== 同时应用多个聚合函数 ====================
# 对每列应用多个统计函数
print(f"\n自定义统计:")
print(grouped.agg(['mean', 'std', 'min', 'max']))
# agg()可接收函数列表,同时计算均值、标准差、最小值、最大值

13.6 transform转换操作

transform返回与原数据相同形状的结果。

列表 13.9: 使用transform进行标准化
# =============================================================================
# 题目:分组标准化转换
# =============================================================================
# 本示例演示使用transform进行组内标准化
# 金融应用:消除不同股票的价格量纲差异,便于比较

# ==================== 标准化转换 ====================
# 标准化公式: (x - mean) / std
# 将每组数据转换为均值为0、标准差为1的分布
result = grouped.transform(lambda x: (x - x.mean()) / x.std())
# transform()对每组应用函数,返回与原数据形状相同的DataFrame
# lambda x是匿名函数,x代表每组的数据

print("标准化结果:")
print(result)  # 输出:原数据被标准化,但形状保持不变

# ==================== 验证标准化结果 ====================
# 验证:每组的均值应约等于0,标准差应约等于1
print(f"\n验证-每组均值:")
print(result.groupby(df['A']).mean())
# 对标准化后的数据再分组计算均值,应接近0

print(f"\n验证-每组标准差:")
print(result.groupby(df['A']).std())
# 对标准化后的数据再分组计算标准差,应接近1

金融应用: 组内标准化,消除组间差异

13.7 filter过滤分组

列表 13.10: 使用filter过滤分组
# =============================================================================
# 题目:按条件过滤分组
# =============================================================================
# 本示例演示如何基于组的统计特征过滤整个组
# 金融应用:筛选出交易量活跃的股票、过滤掉小客户群体

# ==================== 过滤分组 ====================
# 保留C列均值>3的整个组(不是过滤行,是过滤组)
filtered = df.groupby('A').filter(lambda x: x['C'].mean() > 3)
# filter()对每组应用条件函数,返回满足条件的组的所有行
# 逻辑:如果某组的C列平均值>3,则保留该组的所有数据

print("过滤后的数据:")
print(filtered)  # 输出:只包含C列均值>3的组的数据

13.8 apply灵活应用

列表 13.11: 使用apply应用自定义函数
# =============================================================================
# 题目:对每组应用复杂自定义函数
# =============================================================================
# 本示例演示使用apply实现灵活的分组操作
# 金融应用:找出每只股票价格最高的交易日、计算每组的复合增长率等

# ==================== 定义自定义函数 ====================
# 自定义函数:找出每组D列最大的行
def get_max_row(group):
    """找出分组中D列最大的行"""
    return group.loc[group['D'].idxmax()]
# group是每组的完整DataFrame
# idxmax()返回D列最大值的索引(行号)
# loc[]根据索引定位整行数据

# ==================== 对每组应用自定义函数 ====================
result = df.groupby('A').apply(get_max_row)
# apply()对每组应用自定义函数,可返回任意结构的DataFrame
print("每组D列最大的行:")
print(result)  # 输出:每个组中D列最大的那一行数据

13.9 本章小结

要点:

  • groupby 只完成 split,计算由后续动作触发;组名自动成为结果的新索引
  • agg/mean 输出一组一行;transform 输出与原表同形状;filter 按整组去留返回子集;apply 最灵活
  • 对含文本列的整表直接 mean() 会报错,应先用 [['列名', ...]] 选取数值列
  • 平台任务的 (x - x.mean()) / x.std() 组内标准化是 transform 的典型用法

易错点:

  • 期望”每组一行”却用了 transform,或期望”贴回明细”却用了 agg,输出形状立刻不对
  • filter 筛的是”组”不是”行”:结果与布尔行筛选 df[df['C'] > 3] 结构不同
  • groupby('A')[['C', 'D']] 得到数据框分组、groupby('A')['C'] 得到序列分组,可用的后续方法不同
  • 对含分组列的整表直接 apply 在新版 pandas 会触发弃用警告,先选列再 apply 更稳妥

13.10 动手与思考

以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。

  1. 输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。

    import pandas as pd
    
    sales = pd.DataFrame({'营业部': ['甲', '甲', '乙', '乙'], '销售额': [100, 300, 200, 600]})
    print(sales.groupby('营业部')['销售额'].sum())
    print(sales.groupby('营业部')['销售额'].transform('sum'))
    print(sales.groupby('营业部').filter(lambda g: g['销售额'].sum() > 400))

    参考答案(先写下你的预测再点开)

    解题思路:逐行推演。第 1 行 sum():聚合动作,每组压成一个数——甲组 100+300=400,乙组 200+600=800;输出长度等于组数(2 行),组名成为新索引,且组名按编码排序(“乙”排在“甲”前)。第 2 行 transform('sum'):把每组的合计贴回组内每一行,输出与原列同长度(4 行)、索引与原表对齐——前两行都是甲组合计 400,后两行都是乙组合计 800。第 3 行 filter:对每组调用判断函数,返回 True 的组的全部明细行被保留——甲组合计 400 不大于 400,整组出局;乙组 800 大于 400,两行明细都保留。三行代码正好对应“一组一行、同形状贴回、整组去留”三种输出形状。

    # 验证脚本:逐行输出三种分组动作的结果
    import pandas as pd  # 导入Pandas库
    
    sales = pd.DataFrame({'营业部': ['甲', '甲', '乙', '乙'], '销售额': [100, 300, 200, 600]})  # 4行两组的小销售表
    print(sales.groupby('营业部')['销售额'].sum())  # 聚合:每组一个合计,组名为索引
    print(sales.groupby('营业部')['销售额'].transform('sum'))  # 变换:组合计贴回每行,与原表同长度
    print(sales.groupby('营业部').filter(lambda g: g['销售额'].sum() > 400))  # 过滤:合计超400的组整组保留

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    营业部
    乙    800
    甲    400
    Name: 销售额, dtype: int64
    0    400
    1    400
    2    800
    3    800
    Name: 销售额, dtype: int64
      营业部  销售额
    2   乙  200
    3   乙  600

    回扣本章:对应本章小结“要点”第 2 条——agg/mean 输出一组一行,transform 输出与原表同形状,filter 按整组去留返回子集。

  2. 概念辨析:同一列数据,agg('sum')transform('sum') 的输出形状与索引有何不同?各自适合回答什么业务问题?groupby('A')['C']groupby('A')[['C']] 的差别又是什么?

    参考答案(点开前请先独立完成)

    解题思路:逐点作答。第一,agg('sum') 输出长度等于组数,索引是组名,回答的是“每组的汇总指标是多少”,如各营业部合计销售额;transform('sum') 输出与原列同长度、索引与原表对齐(每行填的是它所在组的合计),回答的是“每一行相对本组是什么水平”,典型用法是把组统计量贴回明细后再算组内占比、与组均值的差。第 1 题的前两行输出就是两者的直观对照:一个 2 行、一个 4 行。第二,groupby('A')['C'] 选单列,得到序列分组(SeriesGroupBy),后续 agg/transform 返回 Series;groupby('A')[['C']] 选单列的列表,得到数据框分组(DataFrameGroupBy),后续 agg/transform 返回单列的 DataFrame。两者数值相同,但返回对象类型和可用的后续方法不同——需要把结果作为新列并回原表时,序列分组配 transform 最直接;需要对多列同时分组计算时,用数据框分组。

    回扣本章:对应本章小结“要点”第 2 条与“易错点”第 3 条——groupby('A')[['C', 'D']] 得到数据框分组、groupby('A')['C'] 得到序列分组,可用的后续方法不同。

  3. 变式任务:在本章 8 行销售表上计算各营业部的”件均销售额”(合计销售额除以合计数量),分别用”agg 求出两个合计后再相除”与”apply 返回一个 pd.Series“两种方式实现,比较两种写法。

    参考答案(点开前请先独立完成)

    解题思路:件均销售额 = 组内销售额合计 ÷ 组内数量合计,注意先分别合计再相除,不等于“每行销售额除以数量再平均”。方式一(agg 路线):groupby('营业部')[['销售额', '数量']].agg('sum') 一次得到两个合计列,然后两个 Series 相除——pandas 按营业部索引自动对齐。方式二(apply 路线):定义一个接收组子表的函数,在函数内部完成“两个合计相除”并返回一个 pd.Series,交给 apply 逐组调用;先选列再 apply,避开新版 pandas 对分组列操作的弃用警告。两种写法结果完全一致;方式一直白、适合“汇总后再派生指标”,方式二把任意复杂逻辑封装进函数、适合每组要做多步计算的场景。

    # 变式程序:两种方式计算各营业部件均销售额(销售表数据同 @lst-ch13-sales-table)
    import pandas as pd  # 导入Pandas库
    
    sales = pd.DataFrame({  # 重建本章8行销售表
        '营业部': ['华东一部', '华东一部', '华东二部', '华东二部', '华北一部', '华北一部', '华北一部', '华南一部'],
        '商品': ['笔记本', '平板', '笔记本', '手机', '平板', '耳机', '笔记本', '手机'],
        '销售额': [12000, 8000, 9000, 15000, 6000, 2000, 11000, 13500],
        '数量': [24, 40, 18, 30, 30, 50, 22, 27]
    })
    group_totals = sales.groupby('营业部')[['销售额', '数量']].agg('sum')  # 方式一第1步:分组一次求出两个合计
    per_item_agg = group_totals['销售额'] / group_totals['数量']  # 方式一第2步:合计销售额除以合计数量
    
    def calc_per_item(group):  # 方式二:对每组返回一个pd.Series
        return pd.Series(group['销售额'].sum() / group['数量'].sum(), index=['件均销售额'])  # 组内两个合计相除
    
    per_item_apply = sales.groupby('营业部')[['销售额', '数量']].apply(calc_per_item)  # 先选列再apply,避开对分组列操作的弃用警告
    print(per_item_agg)  # 输出方式一结果
    print(per_item_apply)  # 输出方式二结果

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    营业部
    华东一部    312.50000
    华东二部    500.00000
    华北一部    186.27451
    华南一部    500.00000
    dtype: float64
              件均销售额
    营业部
    华东一部  312.50000
    华东二部  500.00000
    华北一部  186.27451
    华南一部  500.00000

    注意:以上为本题变式的独立代码;平台任务 列表 13.7 对应的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣本章:对应本章小结“要点”第 2 条与“易错点”第 4 条——agg 一组一行适合汇总派生,apply 最灵活;先选列再 apply 可避开新版 pandas 的弃用警告。

  4. 动手验证:把销售表中任意一个销售额改为缺失值,分别运行 sales.groupby('营业部').size()sales.groupby('营业部').count(),解释两者计数结果的差异。

    参考答案(点开前请先独立完成)

    解题思路:把华东一部的第 1 笔销售额(12000)改为缺失值后对照两种计数。size() 数的是行数:只要这行存在就计数,缺失值也计入——华东一部仍是 2。count() 数的是每列的非缺失值个数:逐列统计,华东一部的销售额列只剩 1 个非缺失值(另 1 笔是 NaN),商品列、数量列仍各是 2。因此差异集中体现在含缺失值的那一列上:诊断“每组有多少条记录”用 size(),诊断“每列有效数据有多少”用 count()

    # 验证脚本:对比size与count的计数口径
    import pandas as pd  # 导入Pandas库
    
    sales = pd.DataFrame({  # 重建本章8行销售表
        '营业部': ['华东一部', '华东一部', '华东二部', '华东二部', '华北一部', '华北一部', '华北一部', '华南一部'],
        '商品': ['笔记本', '平板', '笔记本', '手机', '平板', '耳机', '笔记本', '手机'],
        '销售额': [12000, 8000, 9000, 15000, 6000, 2000, 11000, 13500],
        '数量': [24, 40, 18, 30, 30, 50, 22, 27]
    })
    sales.loc[0, '销售额'] = None  # 把华东一部第1笔销售额改为缺失值
    print(sales.groupby('营业部').size())  # size()统计每组的行数,缺失值也计入
    print(sales.groupby('营业部').count())  # count()逐列统计每组的非缺失值个数

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    营业部
    华东一部    2
    华东二部    2
    华北一部    3
    华南一部    1
    dtype: int64
          商品  销售额  数量
    营业部
    华东一部   2    1   2
    华东二部   2    2   2
    华北一部   3    3   3
    华南一部   1    1   1

    回扣本章:对应 列表 13.2 中“各组包含的明细行数”的观察——size() 数行(含缺失),count() 逐列数非缺失值;缺失值的系统处理见第 章节 16 章。